/*******************************************************************************
 * Copyright (c) 2000, 2008 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.jface.text.templates.persistence;

import org.eclipse.che.jface.text.templates.ContextTypeRegistry;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.text.templates.Template;
import org.eclipse.jface.text.templates.TemplateException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * A collection of templates. Clients may instantiate this class. In order to
 * load templates contributed using the <code>org.eclipse.ui.editors.templates</code>
 * extension point, use a <code>ContributionTemplateStore</code>.
 *
 * @since 3.0
 */
public class TemplateStore {
	/** The stored templates. */
	private final List fTemplates = new ArrayList();
	/** The preference store. */
//	private IPreferenceStore    fPreferenceStore;
	/**
	 * The key into <code>fPreferenceStore</code> the value of which holds custom templates
	 * encoded as XML.
	 */
	private String              fKey;
	/**
	 * The context type registry, or <code>null</code> if all templates regardless
	 * of context type should be loaded.
	 */
	private ContextTypeRegistry fRegistry;
	/**
	 * Set to <code>true</code> if property change events should be ignored (e.g. during writing
	 * to the preference store).
	 *
	 * @since 3.2
	 */
	private boolean fIgnorePreferenceStoreChanges = false;
	/**
	 * The property listener, if any is registered, <code>null</code> otherwise.
	 *
	 * @since 3.2
	 */
//	private IPropertyChangeListener fPropertyListener;


	/**
	 * Creates a new template store.
	 *
	 * @param store the preference store in which to store custom templates
	 *        under <code>key</code>
	 * @param key the key into <code>store</code> where to store custom
	 *        templates
	 */
	public TemplateStore(String key) {
//		Assert.isNotNull(store);
		Assert.isNotNull(key);
//		fPreferenceStore = store;
		fKey = key;
	}

	/**
	 * Creates a new template store with a context type registry. Only templates
	 * that specify a context type contained in the registry will be loaded by
	 * this store if the registry is not <code>null</code>.
	 *
	 * @param registry a context type registry, or <code>null</code> if all
	 *        templates should be loaded
	 * @param store the preference store in which to store custom templates
	 *        under <code>key</code>
	 * @param key the key into <code>store</code> where to store custom
	 *        templates
	 */
	public TemplateStore(ContextTypeRegistry registry,/* IPreferenceStore store,*/ String key) {
		this(/*store, */key);
		fRegistry = registry;
	}

	/**
	 * Loads the templates from contributions and preferences.
	 *
	 * @throws IOException if loading fails.
	 */
	public void load() throws IOException {
		fTemplates.clear();
		loadContributedTemplates();
		loadCustomTemplates();
	}

//	/**
//	 * Starts listening for property changes on the preference store. If the configured preference
//	 * key changes, the template store is {@link #load() reloaded}. Call
//	 * {@link #stopListeningForPreferenceChanges()} to remove any listener and stop the
//	 * auto-updating behavior.
//	 *
//	 * @since 3.2
//	 */
//	public final void startListeningForPreferenceChanges() {
//		if (fPropertyListener == null) {
//			fPropertyListener= new IPropertyChangeListener() {
//				public void propertyChange(PropertyChangeEvent event) {
//					/*
//					 * Don't load if we are in the process of saving ourselves. We are in sync anyway after the
//					 * save operation, and clients may trigger reloading by listening to preference store
//					 * updates.
//					 */
//					if (!fIgnorePreferenceStoreChanges && fKey.equals(event.getProperty()))
//						try {
//							load();
//						} catch (IOException x) {
//							handleException(x);
//						}
//				}
//			};
//			fPreferenceStore.addPropertyChangeListener(fPropertyListener);
//		}
//
//	}
//
//	/**
//	 * Stops the auto-updating behavior started by calling
//	 * {@link #startListeningForPreferenceChanges()}.
//	 *
//	 * @since 3.2
//	 */
//	public final void stopListeningForPreferenceChanges() {
//		if (fPropertyListener != null) {
//			fPreferenceStore.removePropertyChangeListener(fPropertyListener);
//			fPropertyListener= null;
//		}
//	}

	/**
	 * Handles an {@link IOException} thrown during reloading the preferences due to a preference
	 * store update. The default is to write to stderr.
	 *
	 * @param x the exception
	 * @since 3.2
	 */
	protected void handleException(IOException x) {
		x.printStackTrace();
	}

	/**
	 * Hook method to load contributed templates. Contributed templates are superseded
	 * by customized versions of user added templates stored in the preferences.
	 * <p>
	 * The default implementation does nothing.</p>
	 *
	 * @throws IOException if loading fails
	 */
	protected void loadContributedTemplates() throws IOException {
	}

	/**
	 * Adds a template to the internal store. The added templates must have
	 * a unique id.
	 *
	 * @param data the template data to add
	 */
	protected void internalAdd(TemplatePersistenceData data) {
		if (!data.isCustom()) {
			// check if the added template is not a duplicate id
			String id= data.getId();
			for (Iterator it= fTemplates.iterator(); it.hasNext();) {
				TemplatePersistenceData d2= (TemplatePersistenceData) it.next();
				if (d2.getId() != null && d2.getId().equals(id))
					return;
			}
			fTemplates.add(data);
		}
	}

	/**
	 * Saves the templates to the preferences.
	 *
	 * @throws IOException if the templates cannot be written
	 */
	public void save() throws IOException {
//		ArrayList custom= new ArrayList();
//		for (Iterator it= fTemplates.iterator(); it.hasNext();) {
//			TemplatePersistenceData data= (TemplatePersistenceData) it.next();
//			if (data.isCustom() && !(data.isUserAdded() && data.isDeleted())) // don't save deleted user-added templates
//				custom.add(data);
//		}
//
//		StringWriter output= new StringWriter();
//		TemplateReaderWriter writer= new TemplateReaderWriter();
//		writer.save((TemplatePersistenceData[]) custom.toArray(new TemplatePersistenceData[custom.size()]), output);
//
//		fIgnorePreferenceStoreChanges= true;
//		try {
//			fPreferenceStore.setValue(fKey, output.toString());
//			if (fPreferenceStore instanceof IPersistentPreferenceStore)
//				((IPersistentPreferenceStore)fPreferenceStore).save();
//		} finally {
//			fIgnorePreferenceStoreChanges= false;
//		}
	}

	/**
	 * Adds a template encapsulated in its persistent form.
	 *
	 * @param data the template to add
	 */
	public void add(TemplatePersistenceData data) {

		if (!validateTemplate(data.getTemplate()))
			return;

		if (data.isUserAdded()) {
			fTemplates.add(data);
		} else {
			for (Iterator it= fTemplates.iterator(); it.hasNext();) {
				TemplatePersistenceData d2= (TemplatePersistenceData) it.next();
				if (d2.getId() != null && d2.getId().equals(data.getId())) {
					d2.setTemplate(data.getTemplate());
					d2.setDeleted(data.isDeleted());
					d2.setEnabled(data.isEnabled());
					return;
				}
			}

			// add an id which is not contributed as add-on
			if (data.getTemplate() != null) {
				TemplatePersistenceData newData= new TemplatePersistenceData(data.getTemplate(), data.isEnabled());
				fTemplates.add(newData);
			}
		}
	}

	/**
	 * Removes a template from the store.
	 *
	 * @param data the template to remove
	 */
	public void delete(TemplatePersistenceData data) {
		if (data.isUserAdded())
			fTemplates.remove(data);
		else
			data.setDeleted(true);
	}

	/**
	 * Restores all contributed templates that have been deleted.
	 */
	public void restoreDeleted() {
		for (Iterator it= fTemplates.iterator(); it.hasNext();) {
			TemplatePersistenceData data= (TemplatePersistenceData) it.next();
			if (data.isDeleted())
				data.setDeleted(false);
		}
	}

	/**
	 * Deletes all user-added templates and reverts all contributed templates.
	 *
	 * @param doSave <code>true</code> if the store should be saved after restoring
	 * @since 3.5
	 */
	public void restoreDefaults(boolean doSave) {
//		String oldValue= null;
//		if (!doSave)
//			oldValue= fPreferenceStore.getString(fKey);
//
//		try {
//			fIgnorePreferenceStoreChanges= true;
//			fPreferenceStore.setToDefault(fKey);
//		} finally {
//			fIgnorePreferenceStoreChanges= false;
//		}
//
//		try {
//			load();
//		} catch (IOException x) {
//			// can't log from jface-text
//			x.printStackTrace();
//		}
//
//		if (oldValue != null) {
//			try {
//				fIgnorePreferenceStoreChanges= true;
//				fPreferenceStore.putValue(fKey, oldValue);
//			} finally {
//				fIgnorePreferenceStoreChanges= false;
//			}
//		}
	}

	/**
	 * Deletes all user-added templates and reverts all contributed templates.
	 * <p>
	 * <strong>Note:</strong> the store will be saved after restoring.
	 * </p>
	 */
	public void restoreDefaults() {
		restoreDefaults(true);
	}

	/**
	 * Returns all enabled templates.
	 *
	 * @return all enabled templates
	 */
	public Template[] getTemplates() {
		return getTemplates(null);
	}

	/**
	 * Returns all enabled templates for the given context type.
	 *
	 * @param contextTypeId the id of the context type of the requested templates, or <code>null</code> if all templates should be returned
	 * @return all enabled templates for the given context type
	 */
	public Template[] getTemplates(String contextTypeId) {
		List templates= new ArrayList();
		for (Iterator it= fTemplates.iterator(); it.hasNext();) {
			TemplatePersistenceData data= (TemplatePersistenceData) it.next();
			if (data.isEnabled() && !data.isDeleted() && (contextTypeId == null || contextTypeId.equals(data.getTemplate().getContextTypeId())))
				templates.add(data.getTemplate());
		}

		return (Template[]) templates.toArray(new Template[templates.size()]);
	}

	/**
	 * Returns the first enabled template that matches the name.
	 *
	 * @param name the name of the template searched for
	 * @return the first enabled template that matches both name and context type id, or <code>null</code> if none is found
	 */
	public Template findTemplate(String name) {
		return findTemplate(name, null);
	}

	/**
	 * Returns the first enabled template that matches both name and context type id.
	 *
	 * @param name the name of the template searched for
	 * @param contextTypeId the context type id to clip unwanted templates, or <code>null</code> if any context type is OK
	 * @return the first enabled template that matches both name and context type id, or <code>null</code> if none is found
	 */
	public Template findTemplate(String name, String contextTypeId) {
		Assert.isNotNull(name);

		for (Iterator it= fTemplates.iterator(); it.hasNext();) {
			TemplatePersistenceData data= (TemplatePersistenceData) it.next();
			Template template= data.getTemplate();
			if (data.isEnabled() && !data.isDeleted()
					&& (contextTypeId == null || contextTypeId.equals(template.getContextTypeId()))
					&& name.equals(template.getName()))
				return template;
		}

		return null;
	}

	/**
	 * Returns the first enabled template that matches the given template id.
	 *
	 * @param id the id of the template searched for
	 * @return the first enabled template that matches id, or <code>null</code> if none is found
	 * @since 3.1
	 */
	public Template findTemplateById(String id) {
		TemplatePersistenceData data= getTemplateData(id);
		if (data != null && !data.isDeleted())
			return data.getTemplate();

		return null;
	}

	/**
	 * Returns all template data.
	 *
	 * @param includeDeleted whether to include deleted data
	 * @return all template data, whether enabled or not
	 */
	public TemplatePersistenceData[] getTemplateData(boolean includeDeleted) {
		List datas= new ArrayList();
		for (Iterator it= fTemplates.iterator(); it.hasNext();) {
			TemplatePersistenceData data= (TemplatePersistenceData) it.next();
			if (includeDeleted || !data.isDeleted())
				datas.add(data);
		}

		return (TemplatePersistenceData[]) datas.toArray(new TemplatePersistenceData[datas.size()]);
	}

	/**
	 * Returns the template data of the template with id <code>id</code> or
	 * <code>null</code> if no such template can be found.
	 *
	 * @param id the id of the template data
	 * @return the template data of the template with id <code>id</code> or <code>null</code>
	 * @since 3.1
	 */
	public TemplatePersistenceData getTemplateData(String id) {
		Assert.isNotNull(id);
		for (Iterator it= fTemplates.iterator(); it.hasNext();) {
			TemplatePersistenceData data= (TemplatePersistenceData) it.next();
			if (id.equals(data.getId()))
				return data;
		}

		return null;
	}

	private void loadCustomTemplates() throws IOException {
//		String pref= fPreferenceStore.getString(fKey);
//		if (pref != null && pref.trim().length() > 0) {
//			Reader input= new StringReader(pref);
//			TemplateReaderWriter reader= new TemplateReaderWriter();
//			TemplatePersistenceData[] datas= reader.read(input);
//			for (int i= 0; i < datas.length; i++) {
//				TemplatePersistenceData data= datas[i];
//				add(data);
//			}
//		}
	}

	/**
	 * Validates a template against the context type registered in the context
	 * type registry. Returns always <code>true</code> if no registry is
	 * present.
	 *
	 * @param template the template to validate
	 * @return <code>true</code> if validation is successful or no context
	 *         type registry is specified, <code>false</code> if validation
	 *         fails
	 */
	private boolean validateTemplate(Template template) {
		String contextTypeId= template.getContextTypeId();
		if (contextExists(contextTypeId)) {
			if (fRegistry != null)
				try {
					fRegistry.getContextType(contextTypeId).validate(template.getPattern());
				} catch (TemplateException e) {
					return false;
				}
			return true;
		}

		return false;
	}

	/**
	 * Returns <code>true</code> if a context type id specifies a valid context type
	 * or if no context type registry is present.
	 *
	 * @param contextTypeId the context type id to look for
	 * @return <code>true</code> if the context type specified by the id
	 *         is present in the context type registry, or if no registry is
	 *         specified
	 */
	private boolean contextExists(String contextTypeId) {
		return contextTypeId != null && (fRegistry == null || fRegistry.getContextType(contextTypeId) != null);
	}

	/**
	 * Returns the registry.
	 *
	 * @return Returns the registry
	 */
	protected final ContextTypeRegistry getRegistry() {
		return fRegistry;
	}
}

